package com.yokead.service.nginxlog;

import cn.hutool.cache.impl.LFUCache;
import cn.hutool.cache.impl.LRUCache;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.LineHandler;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.MD5;
import cn.jiangzeyin.StringUtil;
import com.github.odiszapc.nginxparser.NgxConfig;
import com.github.odiszapc.nginxparser.NgxParam;
import com.yokead.common.Config;
import com.yokead.nginxlog.entity.Nginx_Log_2019;
import com.yokead.system.log.SystemLog;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author yangheng
 */
@Service
public class NginxLogAnalysisService {

    @Resource
    private NginxLog2019Service nginxLog2019Service;

    private static LRUCache<String, String> analyzedLogPathCache = new LRUCache<String, String>(1000000);

    /**
     * 缓存时间。10天 毫秒
     */
    private static final long CACHE_TIME = 864000000L;


    /**
     * 开始分析
     */
    public void startAnalysis() {
        SystemLog.LOG().info("开始分析nginx日志-----------{}", DateUtil.formatDateTime(new DateTime()));
        //获取需要分析的nginx域名和日志的对应关系
        Map<String, Set<String>> needAnalysisMap = listNeedAnalysisMap();
        if (CollUtil.isEmpty(needAnalysisMap)) {
            SystemLog.LOG().info("没有找到需要处理的日志文件。分析结束");
        } else {
            needAnalysisMap.forEach(this::analysisLogs);
        }
        SystemLog.LOG().info("结束分析nginx日志-----------{}", DateUtil.formatDateTime(new DateTime()));
    }


    /**
     * 开始分析日志
     *
     * @param domain     域名
     * @param logPathSet 域名对应的日志文件路径s
     */
    private void analysisLogs(String domain, Set<String> logPathSet) {
        for (String path : logPathSet) {
            String pathMd5 = SecureUtil.md5(path);
            String cacheValue = analyzedLogPathCache.get(pathMd5);

            if (StrUtil.isNotEmpty(cacheValue)) {
                SystemLog.LOG().info("日志文件【{}】已经被分析过了。放弃", path);
                continue;
            }
            if (nginxLog2019Service.isExists(pathMd5)) {
                analyzedLogPathCache.put(path, domain, CACHE_TIME);
                SystemLog.LOG().info("日志文件【{}】已经被分析过了。放弃", path);
                continue;
            }
            analyzedLogPathCache.put(pathMd5, domain, CACHE_TIME);
            File logFile = new File(path);
            this.analysisLogFile(domain, logFile);
        }
    }

    /**
     * 分析单个日志文件
     *
     * @param domain  域名。
     * @param logFile 日志文件
     */
    private void analysisLogFile(String domain, File logFile) {
        SystemLog.LOG().info("开始解析：" + logFile.getPath());
        ArrayList<Nginx_Log_2019> logEntityList = new ArrayList<>();
        FileUtil.readUtf8Lines(logFile, (LineHandler) line -> {
            //左边边界角标
            int leftIndex = line.indexOf(" ");
            // ip
            String ip = line.substring(0, leftIndex);
            line = line.substring(leftIndex);
            // time
            leftIndex = line.indexOf("[");
            int rightIndex = line.indexOf("]");
            String timeStr = line.substring(leftIndex + 1, rightIndex);
            long time = formatTime(timeStr);
            line = line.substring(rightIndex + 2);
            // type
            leftIndex = line.indexOf("\"");
            rightIndex = line.indexOf(" ");
            String type = line.substring(leftIndex + 1, rightIndex);
            // url
            line = line.substring(rightIndex + 1);
            rightIndex = line.indexOf(" ");
            String url = line.substring(0, rightIndex);
            url = convertUrl(domain, url);
            if (StrUtil.isEmpty(url)) {
                return;
            }
            // status
            line = line.substring(rightIndex + 1);
            leftIndex = line.indexOf(" ");
            line = line.substring(leftIndex + 1);
            leftIndex = line.indexOf(" ");
            String status = line.substring(0, leftIndex);
            // referer
            line = line.substring(leftIndex + 1);
            leftIndex = line.indexOf("\"");
            line = line.substring(leftIndex + 1);
            leftIndex = line.indexOf("\"");
            String referer = line.substring(0, leftIndex);
            // uag
            line = line.substring(leftIndex + 1);
            leftIndex = line.indexOf("\"");
            line = line.substring(leftIndex + 1);
            leftIndex = line.indexOf("\"");
            String uag = line.substring(0, leftIndex);
            // x-ip
            line = line.substring(leftIndex + 1);
            leftIndex = line.indexOf("\"");
            rightIndex = line.lastIndexOf("\"");
            String xIp = line.substring(leftIndex + 1, rightIndex);

            String pathMd5 = SecureUtil.md5(logFile.getPath());

            Nginx_Log_2019 logEntity = new Nginx_Log_2019();
            logEntity.setIp(ip);
            logEntity.setXip(xIp);
            logEntity.setDomain(domain);
            logEntity.setFilePath(pathMd5);
            logEntity.setReferer(referer);
            logEntity.setStatus(Convert.toInt(status, 0));
            logEntity.setTime(time);
            logEntity.setType(type);
            logEntity.setUag(uag);
            logEntity.setUrl(url);
            logEntityList.add(logEntity);
        });
        if (CollUtil.isNotEmpty(logEntityList)) {
            nginxLog2019Service.insertList(logEntityList);
        }
        SystemLog.LOG().info("文件解析完毕：" + logFile.getPath());
    }


    private static final String URL_FAVICON = "/favicon.ico";

    /**
     * 转化url
     *
     * @param domain 域名
     * @param url    链接
     * @return String
     */
    private String convertUrl(String domain, String url) {
        if (URL_FAVICON.equalsIgnoreCase(url)) {
            return null;
        }
        if ("/".equals(url)) {
            return "/index.html";
        } else if (url.endsWith("/")) {
            return url + "index.html";
        }
        return URLUtil.complateUrl(domain, url);
    }


    private long formatTime(String timeStr) {
        SimpleDateFormat format = new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss Z", Locale.ENGLISH);
        long time = 0L;
        try {
            time = format.parse(timeStr).getTime() / 1000L;
        } catch (ParseException ignored) {
        }
        return time;
    }


    /**
     * 获取需要分析的nginx域名和日志的对应关系
     *
     * @return Map<域名, 需要被分析的日志文件集合>
     */
    private Map<String, Set<String>> listNeedAnalysisMap() {
        //nginx配置文件的目录  /etc/nginx/conf.d/ad
        String nginxConfigPath = Config.Nginx.getNginxConfigPath();
        File nginxConfigDir = new File(nginxConfigPath);
        if (!FileUtil.isDirectory(nginxConfigDir)) {
            SystemLog.ERROR().error("未找到nginx-ad配置文件夹。操作失败");
            return null;
        }
        HashMap<String, Set<String>> demonANdLogsMap = new HashMap<>(20);

        Stack<File> nginxConfigFileStack = new Stack<>();
        nginxConfigFileStack.push(nginxConfigDir);
        // 遍历类路径
        while (!nginxConfigFileStack.isEmpty()) {
            File item = nginxConfigFileStack.pop();
            File[] nginxConfigFiles = item.listFiles((dir, name) -> new File(dir, name).isDirectory() || name.endsWith(".conf"));
            if (ArrayUtil.isEmpty(nginxConfigFiles)) {
                continue;
            }
            for (File nginxConfigFile : nginxConfigFiles) {
                if (nginxConfigFile.isDirectory()) {
                    nginxConfigFileStack.push(nginxConfigFile);
                    continue;
                }
                //分析一个配置文件中域名和日志的对应map
                try {
                    demonANdLogsMap.putAll(analysisNginxConfigFile(nginxConfigFile));
                } catch (Exception e) {
                    SystemLog.ERROR().error("读取nginx配置文件失败 文件名:" + nginxConfigFile.getPath(), e);
                }
            }
        }
        return demonANdLogsMap;
    }

    /**
     * 分析nginx配置文件。分析单个文件
     *
     * @param nginxConfigFile nginx配置文件
     * @return Map<域名, 需要被分析的日志文件集合>
     * @throws IOException ex
     */
    private Map<String, Set<String>> analysisNginxConfigFile(File nginxConfigFile) throws IOException {
        NgxConfig conf = NgxConfig.read(nginxConfigFile.getPath());
        NgxParam accessLog = conf.findParam("server", "access_log");
        String[] logInfo = StringUtil.stringToArray(accessLog.getValue(), " ");
        if (logInfo == null) {
            throw new IllegalArgumentException(nginxConfigFile.getPath() + " 日志配置解析失败");
        }
        String path = logInfo[0];

        File logFile = new File(path);
        if (!logFile.isFile()) {
            throw new IllegalArgumentException(nginxConfigFile.getPath() + " 日志文件不正确：" + logFile.getPath());
        }
        NgxParam serverName = conf.findParam("server", "server_name");
        if (serverName == null) {
            throw new IllegalArgumentException(nginxConfigFile.getPath() + " 没有配置域名");
        }
        List<String> stringList = serverName.getValues();
        if (CollUtil.isEmpty(stringList)) {
            throw new IllegalArgumentException(nginxConfigFile.getPath() + " 没有配置域名");
        }
        String doMain = stringList.get(0);
        String logName = logFile.getName();
        File[] fileBacks = logFile.getParentFile().listFiles((dir, name) -> !new File(dir, name).isDirectory() && !name.endsWith(".log") && name.startsWith(logName));
        if (ArrayUtil.isEmpty(fileBacks)) {
            throw new IllegalArgumentException(nginxConfigFile.getPath() + " 没有找到需要分析的日志文件");
        }
        HashMap<String, Set<String>> map = new HashMap<>(1);
        map.put(doMain, Arrays.stream(fileBacks).map(File::getPath).collect(Collectors.toSet()));
        return map;
    }
}
